构建优秀的 NPM 包
•
# 构建优秀的 NPM 包
```txt
// https://twitter.com/acemarke/status/1652021889307496448
Things I have to keep in mind when publishing a library in 2023:
- Build artifact formats (ESM, CJS, UMD)
- Matrixed with: dev/prod/NODE_ENV builds
- Bundled or individual .js per source
- `exports` setup
- Webpack 4 limits
- TS `moduleResolution` options
- User environments
```
## 一个优秀的 npm 包应该具有哪些特性
- 同时提供 CommonJS、ESM 支持
- 提供 Typescript 类型支持
- 精简的体积
- Tree Shaking
- 压缩
- 发布仅包含关键文件
- 明确依赖和使用环境
- 安全
- 支持 esm.sh、unpkg
- 令人舒服的 LICENSE
## 拆解
### 模块导出及类型
由于历史因素的存在,Node.js 最早支持的模块类型为 CommonJS,随着 ES6 的发布带来全新的 ESM 模块类型,整个 JS 生态也在逐渐向 ESM 靠拢。比如 Vite 已经表示从 v6 版本开始,不再提供 CJS 导出的 Vite 包(<https://vitejs.dev/guide/troubleshooting.html#vite-cjs-node-api-deprecated)。推荐将新的模块都按照> ESM 模块编写,并在 package.json 中添加 `"type": "module"`,加速整个 ESM 模块生态的发展。
ESM 中有一些特性在 CommonJS 中是不支持的,因此无法在 CommonJS 项目中引入 ESM 模块,但 ESM 模块可以通过一些手段引用 CommonJS。
但事实上由于 npm 生态及公司业务中仍然有大量存量的 CommonJS 模块和项目,为了兼容性考虑,也可以不添加 "type": "model",通过 package.json 中的 `module`、`exports` 等字段提供 ESM 的兼容。
#### 主流的导出优先级为:[^5]
##### Node.js
export > main
##### 打包工具
browser > export > module > main
#### ESM only
```json
{
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts"
"export": "dist/index.js",
}
```
#### CJS 兼容 ESM
- mian 导出 UMD,同时支持 CJS 和 AMD,并且会导出全局变量方便在浏览器中直接使用。
- module 字段并没有被 node.js 官方纳入标准,但类似 Rollup、Webpack 等打包工具会判断这个字段。<https://stackoverflow.com/questions/42708484/what-is-the-module-package-json-field-for>
- export 字段非常强大。<https://nodejs.org/api/packages.html#conditional-exports>
- 要为 .mjs 单独提供一份 .mts 文件作为类型。<https://publint.dev/rules#export_types_invalid_format>
```json
{
"main": "dist/index.umd.min.js",
"types": "dist/index.d.ts",
"module": "dist/index.mjs",
"export": {
".": {
"require": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"import": {
"types": "./dist/index.d.mts",
"default": "./dist/index.mjs"
}
}
}
}
```
### 精简体积
#### Three shaking
主流的打包工具都支持 three shaking,前提是使用 `import` 和 `export`,依赖必须兼容 ESM 的导出。由于 JavaScript 过于灵活,所以它有两个概念 usedExports 和 sideEffects。
- 模块要提供 ESM 的导出
- 无副作用
- package.json 中 "sideEffects": false or "sideEffects": ["src/xx.js"],代表的是模块级别的无副作用,即整个项目或某些文件中的所有代码都无副作用。
- terser 中可以使用 `/*#**__PURE*__/` 的行内注解表示具体那些代码是无副作用的
更详细的内容: <https://webpack.js.org/guides/tree-shaking>
#### 压缩
基于 terser 和打包工具的普遍性,也为了方便对程序进行调试,大部分产物都不需要进行压缩。
umd 提供在浏览器中直接使用的可能性,尽可能提供 minify 后的版本。
#### 仅发布有用的文件
通过配置 package.json 中的 files 字段,来指定发布包含哪些文件,减少使用包的人的下载时间和磁盘占用。
```json
{
"files": ["dist"]
}
```
### 明确依赖和使用环境
TODO
### 安全
一个库中很可能引用一些其他的库,为了确保不会存在明显的安全问题。需要一些手段检测代码本身或依赖是否存在漏洞。
- <https://docs.npmjs.com/cli/v10/commands/npm-audit>
- <https://snyk.io>
### CDN 分发的特有配置
TODO
### 选择 LICENSE
可参考阮一峰的 <https://www.ruanyifeng.com/blog/2011/05/how_to_choose_free_software_licenses.html>
推荐一种比 MIT 限制更宽松的 LICENSE: Unlicense <https://unlicense.org/>
### 检查 package.json 的工具
检查包对各种打包工具的兼容性及配置路径是否正确
- [publint](https://www.npmjs.com/package/publint)
- [arethetypeswrong/cli](@arethetypeswrong/cli)
## Tools
<https://npmgraph.js.org> pcakge.json 依赖可视化
<https://pkg-size.dev> 快速查看包体积
<https://bundlephobia.com> 包体积,带有 badge
### 参考
[1] <https://github.com/frehner/modern-guide-to-packaging-js-library>
[2] <https://blog.isquaredsoftware.com/2023/08/esm-modernization-lessons/>
[3] <https://github.com/stereobooster/package.json>
[4] <https://esbuild.github.io/api/#main-fields>
[5] <https://juejin.cn/post/7225072417532739644>
[6] <https://www.totaltypescript.com/how-to-create-an-npm-package>